﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace PacketRush.Core
{
    /// <summary>
    /// Shares common functionality between <see cref="PacketRush.Core.ReliableUdpClientNetworkingProvider"/>
    /// and <see cref="PacketRush.Core.ReliableUdpServerNetworkingProvider"/>.
    /// </summary>
    public abstract class ReliableUdpNetworkingProvider : INetworkingProvider
    {
        private PacketWriter _PacketWriter = new PacketWriter();
        /// <summary>
        /// A shared packet writer.
        /// </summary>
        protected PacketWriter PacketWriter
        {
            get
            {
                return _PacketWriter;
            }
        }

        /// <summary>
        /// Used as a temporary buffer for Socket.ReceiveFrom
        /// </summary>
        private byte[] _ReceiveBuffer = new byte[Packet.MaxPacketSize];

        /// <summary>
        /// The single socket for UDP networking.
        /// </summary>
        protected Socket Socket { get; set; }

        /// <summary>
        /// Raised when this provider encounters an error.
        /// </summary>
        public Action<NetErrorEventArgs> ErrorAction { get; set; }

        private ReliableUdpSettings _Settings;
        /// <summary>
        /// Default constructor
        /// </summary>
        public ReliableUdpNetworkingProvider() : this(new ReliableUdpSettings())
        {

        }

        /// <summary>
        /// Constructor with settings.
        /// </summary>
        /// <param name="settings">Settings for this provider</param>
        public ReliableUdpNetworkingProvider(ReliableUdpSettings settings)
        {
            _Settings = settings;
        }

        /// <summary>
        /// Send a packet to the given connection reliably.
        /// </summary>
        /// <param name="target">The target connection to send to.</param>
        /// <param name="packet">The raw packet to send.</param>
        public void SendReliable(INetConnection target, byte[] packet)
        {
            ReliableUdpConnection conn = (ReliableUdpConnection)target;
            conn.AddToAckWaiting(packet, RunningTime.ElapsedMilliseconds);
            Socket.SendTo(packet, target.RemoteEndPoint);
        }

        /// <summary>
        /// Send a packet to the given connection unreliably.
        /// </summary>
        /// <param name="target">The target connection to send to.</param>
        /// <param name="packet">The raw packet to send.</param>
        public void SendUnreliable(INetConnection target, byte[] packet)
        {
            try
            {
                Socket.SendTo(packet, target.RemoteEndPoint);
            }
            catch (SocketException x)
            {
                ErrorAction(new NetErrorEventArgs(target, x.SocketErrorCode));
            }
        }

        /// <summary>
        /// Receives network packets.
        /// </summary>
        public void Receive()
        {
            EndPoint endPoint = null;
            ReliableUdpConnection connection = null;

            while (Socket.Available > 0)
            {
                endPoint = new IPEndPoint(0, 0);

                try
                {
                    int datagramSize = Socket.ReceiveFrom(_ReceiveBuffer, ref endPoint);

                    // Read off some useful data
                    PacketFlags packetFlags = Packet.ReadPacketFlags(_ReceiveBuffer);
                    int serviceId = Packet.ReadServiceId(_ReceiveBuffer);

                    // Look up the corresponding connection
                    connection = LookupConnection(endPoint, serviceId);

                    // Do not service unknown connections unless they are connecting
                    if (connection == null) continue;

                    // Now we know we definetly have a connection, copy the receive buffer
                    // into a suitably sized byte array
                    byte[] rawPacket = new byte[datagramSize];
                    Buffer.BlockCopy(_ReceiveBuffer, 0, rawPacket, 0, datagramSize);

                    if ((packetFlags & PacketFlags.Reliable) != PacketFlags.None)
                    {
                        if ((packetFlags & PacketFlags.Ack) != PacketFlags.None)
                        {
                            if (connection.RemoveFromAckWaiting(rawPacket))
                            {
                                ProcessReceivedAckPacket(rawPacket, serviceId, connection);
                            }
                        }
                        else
                        {
                            ProcessReceivedReliablePacket(rawPacket, serviceId, connection);
                        }
                    }
                    else
                    {
                        ProcessReceivedPacket(rawPacket, serviceId, connection);
                    }
                }
                catch (SocketException x)
                {
                    ErrorAction(new NetErrorEventArgs(connection, x.SocketErrorCode));
                    //
                    // The OnError calls Disconnect in NetClient which in turn
                    // will call this.Disconnect and set Socket to null, if the exception
                    // happens to be a socket exception, so at this point
                    // if the Socket is null we should exit
                    //
                    if (Socket == null) break;

                }
            }
        }

        /// <summary>
        /// Processes a received reliable packet.
        /// </summary>
        /// <remarks>
        /// Performs core logic for reliably receiving a packet send by UDP.
        /// </remarks>
        /// <param name="rawPacket">The packet received.</param>
        /// <param name="serviceId">The service id of the destined service for the packet.</param>
        /// <param name="connection">The connection this packet came from.</param>
        protected void ProcessReceivedReliablePacket(byte[] rawPacket, int serviceId, ReliableUdpConnection connection)
        {
            uint packetSeqIndex = Packet.ReadSequenceIndex(rawPacket);

            //
            // This is exactly the packet we are expecting next.
            //
            if (packetSeqIndex == connection.NextExpectedPacketSequenceIndex)
            {
                // This is the next packet we are expecting, send an ack
                // and increment to the next expected seq index, then
                // pass the packet up to the application
                connection.NextExpectedPacketSequenceIndex++;
                ProcessReceivedPacket(rawPacket, serviceId, connection);
                SendAck(connection, serviceId, packetSeqIndex);
                return;
            }

            //
            // This packet is either a previous packet (duplicate) or its
            // one that we are waiting for (expecting).
            //
            if (packetSeqIndex < connection.NextExpectedPacketSequenceIndex)
            {
                if (connection.ExpectingPacket(packetSeqIndex))
                {
                    // we are expecting this one, so send ack and remove it
                    // from the expecting list, then pass it up to the application
                    SendAck(connection, serviceId, packetSeqIndex);
                    connection.ForgetExpecting(packetSeqIndex);
                    ProcessReceivedPacket(rawPacket, serviceId, connection);
                }
                else
                {
                    // Must be a dupe, send ack anyway but do nothing
                    SendAck(connection, serviceId, packetSeqIndex);
                }

                return;

            }

            //
            // This is a future packet, we are missing some packets inbetween
            //
            if (packetSeqIndex > connection.NextExpectedPacketSequenceIndex)
            {
                // we have recieved a packet thats greater than the next expected
                // sequence index, this means that we are possibly missing some packets
                // inbetween

                // Add each packet between to the expecting list
                uint expected = connection.NextExpectedPacketSequenceIndex;
                while (expected < packetSeqIndex)
                {
                    connection.RememberExpecting(expected);
                    expected++;
                }

                // set the next sequence index to the packetId + 1
                connection.NextExpectedPacketSequenceIndex = packetSeqIndex + 1;

                // Send an ack for this packet and pass it up to the application
                SendAck(connection, serviceId, packetSeqIndex);
                ProcessReceivedPacket(rawPacket, serviceId, connection);
                return;
            }
        }

        /// <summary>
        /// Called during a <see cref="PacketRush.Core.ReliableUdpNetworkingProvider.Receive"/> when a packet is received.
        /// </summary>
        /// <param name="rawPacket">The packet received.</param>
        /// <param name="serviceId">The service id of the service raw packet is destined for.</param>
        /// <param name="connection">The connection the packet was received from.</param>
        protected abstract void ProcessReceivedPacket(byte[] rawPacket, int serviceId, ReliableUdpConnection connection);

        /// <summary>
        /// Called during a <see cref="PacketRush.Core.ReliableUdpNetworkingProvider.Receive"/> when an 
        /// acknowledgement packet is received.
        /// </summary>
        /// <param name="rawPacket">The packet received.</param>
        /// <param name="serviceId">The service id of the service raw packet is destined for.</param>
        /// <param name="connection">The connection the packet was received from.</param>
        protected abstract void ProcessReceivedAckPacket(byte[] rawPacket, int serviceId, ReliableUdpConnection connection);

        /// <summary>
        /// Sends an acknowledgement packet.
        /// </summary>
        /// <param name="connection">The connection to send the acknowledgement to.</param>
        /// <param name="serviceId">The service id of the original packet we are acknowledging.</param>
        /// <param name="packetId">The packet id of the packet we are acknowledging.</param>
        protected void SendAck(ReliableUdpConnection connection, int serviceId, uint packetId)
        {
            // TODO: we could optimise this by sending many acks in one packet
            PacketWriter.Reset();
            PacketWriter.Write(packetId);
            byte[] rawPacket = PacketWriter.ToRawPacket(serviceId, PacketFlags.Reliable | PacketFlags.Ack);

            try
            {
                Socket.SendTo(rawPacket, connection.RemoteEndPoint);
            }
            catch (SocketException x)
            {
                ErrorAction(new NetErrorEventArgs(connection, x.SocketErrorCode));
            }
        }

        /// <summary>
        /// Disconnect this provider from the network.
        /// </summary>
        public abstract void Disconnect();

        /// <summary>
        /// Perform any other work.
        /// </summary>
        public abstract void Update();

        /// <summary>
        /// Indicates the running time since this networking provider was started.
        /// </summary>
        public abstract NetTime RunningTime { get; set; }

        /// <summary>
        /// Looks up a connection by endpoint.
        /// </summary>
        /// <param name="endPoint">The endpoint to lookup the connection.</param>
        /// <param name="serviceId">The service id of the packet that was received.</param>
        /// <returns>A connection.</returns>
        protected abstract ReliableUdpConnection LookupConnection(EndPoint endPoint, int serviceId);

        /// <summary>
        /// Called when the provider receives packets during a <see cref="PacketRush.Core.ReliableUdpNetworkingProvider.Receive"/>.
        /// </summary>
        public Action<byte[], INetConnection> ReceivePacketAction { get; set; }

    }
}